package com.example.webupload.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.webupload.common.BusinessException;
import com.example.webupload.common.Constants;
import com.example.webupload.dto.ChunkMergeDTO;
import com.example.webupload.dto.ChunkUploadDTO;
import com.example.webupload.dto.FileUploadDTO;
import com.example.webupload.entity.FileChunk;
import com.example.webupload.entity.FileInfo;
import com.example.webupload.entity.FileUploadLog;
import com.example.webupload.mapper.FileChunkMapper;
import com.example.webupload.mapper.FileInfoMapper;
import com.example.webupload.mapper.FileUploadLogMapper;
import com.example.webupload.service.FileService;
import com.example.webupload.util.FileTypeUtil;
import com.example.webupload.vo.FileInfoVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 文件服务实现类
 */
@Slf4j
@Service
public class FileServiceImpl implements FileService {

    @Value("${file.upload.path}")
    private String uploadPath;

    @Value("${file.upload.allowed-types}")
    private String allowedTypes;

    @Value("${file.upload.max-size}")
    private Long maxFileSize;

    @Value("${server.servlet.context-path}")
    private String contextPath;

    @Autowired
    private FileInfoMapper fileInfoMapper;

    @Autowired
    private FileChunkMapper fileChunkMapper;

    @Autowired
    private FileUploadLogMapper fileUploadLogMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfoVO uploadFile(FileUploadDTO fileUploadDTO, HttpServletRequest request) {
        MultipartFile file = fileUploadDTO.getFile();
        String md5 = fileUploadDTO.getMd5();
        
        // 检查文件是否为空
        if (file == null || file.isEmpty()) {
            throw new BusinessException(400, "上传文件不能为空");
        }
        
        // 获取原始文件名和扩展名
        String originalFilename = file.getOriginalFilename();
        String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
        
        // 检查文件类型
        checkFileType(extension);
        
        // 检查文件大小
        checkFileSize(file.getSize());
        
        // 如果提供了MD5，检查是否可以秒传
        if (StringUtils.hasText(md5)) {
            FileInfoVO existingFile = checkFileMd5(md5);
            if (existingFile != null) {
                log.info("文件秒传成功：{}", originalFilename);
                return existingFile;
            }
        } else {
            // 如果没有提供MD5，则计算文件的MD5值
            try {
                md5 = DigestUtils.md5DigestAsHex(file.getInputStream());
            } catch (IOException e) {
                log.error("计算文件MD5失败：{}", e.getMessage());
                throw new BusinessException("计算文件MD5失败");
            }
        }
        
        // 生成新的文件名
        String newFilename = generateNewFilename(extension);
        
        // 根据年月创建目录
        String relativePath = createYearMonthDir();
        File targetDir = new File(uploadPath + relativePath);
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        
        // 保存文件
        File targetFile = new File(targetDir, newFilename);
        try {
            file.transferTo(targetFile);
        } catch (IOException e) {
            log.error("保存文件失败：{}", e.getMessage());
            throw new BusinessException("保存文件失败");
        }
        
        // 保存文件信息到数据库
        FileInfo fileInfo = new FileInfo();
        fileInfo.setOriginalName(originalFilename);
        fileInfo.setNewName(newFilename);
        fileInfo.setFilePath(relativePath + newFilename);
        fileInfo.setFileSize(file.getSize());
        fileInfo.setFileType(FileTypeUtil.getFileType(extension));
        fileInfo.setFileExt(extension);
        fileInfo.setMd5(md5);
        fileInfo.setStatus(Constants.FileStatus.VALID);
        fileInfo.setDownloadCount(0);
        fileInfo.setCreateTime(LocalDateTime.now());
        fileInfo.setUpdateTime(LocalDateTime.now());
        fileInfoMapper.insert(fileInfo);
        
        // 记录上传日志
        saveUploadLog(fileInfo.getId(), originalFilename, file.getSize(), FileTypeUtil.getFileType(extension), 
                getClientIp(request), Constants.UploadStatus.SUCCESS, null);
        
        // 返回文件信息
        return convertToFileInfoVO(fileInfo);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean uploadChunk(ChunkUploadDTO chunkUploadDTO, HttpServletRequest request) {
        MultipartFile file = chunkUploadDTO.getFile();
        String fileMd5 = chunkUploadDTO.getFileMd5();
        Integer chunkNumber = chunkUploadDTO.getChunkNumber();
        Long chunkSize = chunkUploadDTO.getChunkSize();
        Integer totalChunks = chunkUploadDTO.getTotalChunks();
        Long totalSize = chunkUploadDTO.getTotalSize();
        String originalFilename = chunkUploadDTO.getOriginalFilename();
        
        // 检查文件是否为空
        if (file == null || file.isEmpty()) {
            throw new BusinessException(400, "上传分片不能为空");
        }
        
        // 检查文件大小
        if (totalSize > maxFileSize) {
            throw new BusinessException(400, "文件大小超出限制");
        }
        
        // 获取扩展名
        String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
        
        // 检查文件类型
        checkFileType(extension);
        
        // 创建分片存储目录
        String chunkDir = uploadPath + "/chunks/" + fileMd5;
        File dir = new File(chunkDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        
        // 保存分片
        File chunkFile = new File(dir, chunkNumber.toString());
        try {
            file.transferTo(chunkFile);
        } catch (IOException e) {
            log.error("保存分片失败：{}", e.getMessage());
            throw new BusinessException("保存分片失败");
        }
        
        // 保存分片信息到数据库
        FileChunk fileChunk = new FileChunk();
        fileChunk.setFileMd5(fileMd5);
        fileChunk.setChunkNumber(chunkNumber);
        fileChunk.setChunkSize(file.getSize());
        fileChunk.setChunkPath("/chunks/" + fileMd5 + "/" + chunkNumber);
        fileChunk.setTotalChunks(totalChunks);
        fileChunk.setTotalSize(totalSize);
        fileChunk.setCreateTime(LocalDateTime.now());
        
        // 检查分片是否已存在
        LambdaQueryWrapper<FileChunk> queryWrapper = Wrappers.<FileChunk>lambdaQuery()
                .eq(FileChunk::getFileMd5, fileMd5)
                .eq(FileChunk::getChunkNumber, chunkNumber);
        FileChunk existingChunk = fileChunkMapper.selectOne(queryWrapper);
        if (existingChunk != null) {
            // 分片已存在，更新信息
            fileChunk.setId(existingChunk.getId());
            fileChunkMapper.updateById(fileChunk);
        } else {
            // 分片不存在，插入新记录
            fileChunkMapper.insert(fileChunk);
        }
        
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfoVO mergeChunks(ChunkMergeDTO chunkMergeDTO, HttpServletRequest request) {
        String fileMd5 = chunkMergeDTO.getFileMd5();
        String originalFilename = chunkMergeDTO.getOriginalFilename();
        Long totalSize = chunkMergeDTO.getTotalSize();
        Integer totalChunks = chunkMergeDTO.getTotalChunks();
        
        // 检查文件大小
        if (totalSize > maxFileSize) {
            throw new BusinessException(400, "文件大小超出限制");
        }
        
        // 获取扩展名
        String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
        
        // 检查文件类型
        checkFileType(extension);
        
        // 检查所有分片是否都已上传
        List<Integer> uploadedChunks = getUploadedChunks(fileMd5);
        if (uploadedChunks.size() != totalChunks) {
            throw new BusinessException(400, "分片不完整，请重新上传");
        }
        
        // 根据年月创建目录
        String relativePath = createYearMonthDir();
        File targetDir = new File(uploadPath + relativePath);
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        
        // 生成新的文件名
        String newFilename = generateNewFilename(extension);
        File targetFile = new File(targetDir, newFilename);
        
        // 合并分片
        try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            int len;
            
            // 按顺序合并分片
            for (int i = 0; i < totalChunks; i++) {
                File chunkFile = new File(uploadPath + "/chunks/" + fileMd5 + "/" + i);
                if (!chunkFile.exists()) {
                    throw new BusinessException("分片文件不存在：" + i);
                }
                
                try (FileInputStream inputStream = new FileInputStream(chunkFile)) {
                    while ((len = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, len);
                    }
                }
            }
            
            outputStream.flush();
        } catch (IOException e) {
            log.error("合并分片失败：{}", e.getMessage());
            throw new BusinessException("合并分片失败");
        }
        
        // 验证合并后的文件MD5
        String mergedFileMd5;
        try (FileInputStream inputStream = new FileInputStream(targetFile)) {
            mergedFileMd5 = DigestUtils.md5DigestAsHex(inputStream);
        } catch (IOException e) {
            log.error("计算合并文件MD5失败：{}", e.getMessage());
            throw new BusinessException("计算合并文件MD5失败");
        }
        
        if (!fileMd5.equals(mergedFileMd5)) {
            // MD5不一致，删除合并后的文件
            targetFile.delete();
            throw new BusinessException("文件MD5校验失败，请重新上传");
        }
        
        // 保存文件信息到数据库
        FileInfo fileInfo = new FileInfo();
        fileInfo.setOriginalName(originalFilename);
        fileInfo.setNewName(newFilename);
        fileInfo.setFilePath(relativePath + newFilename);
        fileInfo.setFileSize(totalSize);
        fileInfo.setFileType(FileTypeUtil.getFileType(extension));
        fileInfo.setFileExt(extension);
        fileInfo.setMd5(fileMd5);
        fileInfo.setStatus(Constants.FileStatus.VALID);
        fileInfo.setDownloadCount(0);
        fileInfo.setCreateTime(LocalDateTime.now());
        fileInfo.setUpdateTime(LocalDateTime.now());
        fileInfoMapper.insert(fileInfo);
        
        // 记录上传日志
        saveUploadLog(fileInfo.getId(), originalFilename, totalSize, FileTypeUtil.getFileType(extension), 
                getClientIp(request), Constants.UploadStatus.SUCCESS, null);
        
        // 清理分片文件
        cleanChunks(fileMd5);
        
        // 返回文件信息
        return convertToFileInfoVO(fileInfo);
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfoVO checkFileMd5(String md5) {
        if (!StringUtils.hasText(md5)) {
            return null;
        }
        
        LambdaQueryWrapper<FileInfo> queryWrapper = Wrappers.<FileInfo>lambdaQuery()
                .eq(FileInfo::getMd5, md5)
                .eq(FileInfo::getStatus, Constants.FileStatus.VALID);
        FileInfo fileInfo = fileInfoMapper.selectOne(queryWrapper);
        
        if (fileInfo != null) {
            return convertToFileInfoVO(fileInfo);
        }
        
        return null;
    }

    public List<Integer> getUploadedChunks(String fileMd5) {
        LambdaQueryWrapper<FileChunk> queryWrapper = Wrappers.<FileChunk>lambdaQuery()
                .eq(FileChunk::getFileMd5, fileMd5)
                .select(FileChunk::getChunkNumber);
        List<FileChunk> chunks = fileChunkMapper.selectList(queryWrapper);
        
        return chunks.stream()
                .map(FileChunk::getChunkNumber)
                .collect(Collectors.toList());
    }

    @Override
    public void downloadFile(Long fileId, HttpServletResponse response) {
        FileInfo fileInfo = fileInfoMapper.selectById(fileId);
        if (fileInfo == null || fileInfo.getStatus() == Constants.FileStatus.INVALID) {
            throw new BusinessException(404, "文件不存在");
        }
        
        File file = new File(uploadPath + fileInfo.getFilePath());
        if (!file.exists()) {
            throw new BusinessException(404, "文件不存在");
        }
        
        // 设置响应头
        response.setContentType("application/octet-stream");
        response.setHeader("Content-Disposition", "attachment;filename=" + 
                encodeFilename(fileInfo.getOriginalName()));
        response.setHeader("Content-Length", String.valueOf(file.length()));
        
        // 写入响应流
        try (FileInputStream inputStream = new FileInputStream(file);
             ServletOutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            outputStream.flush();
            
            // 更新下载次数
            fileInfo.setDownloadCount(fileInfo.getDownloadCount() + 1);
            fileInfoMapper.updateById(fileInfo);
        } catch (IOException e) {
            log.error("下载文件失败：{}", e.getMessage());
            throw new BusinessException("下载文件失败");
        }
    }

    @Override
    public void previewFile(Long fileId, HttpServletResponse response) {
        FileInfo fileInfo = fileInfoMapper.selectById(fileId);
        if (fileInfo == null || fileInfo.getStatus() == Constants.FileStatus.INVALID) {
            throw new BusinessException(404, "文件不存在");
        }
        
        File file = new File(uploadPath + fileInfo.getFilePath());
        if (!file.exists()) {
            throw new BusinessException(404, "文件不存在");
        }
        
        // 设置响应头
        String contentType = getContentType(fileInfo.getFileExt());
        response.setContentType(contentType);
        if (!contentType.startsWith("image/") && !contentType.startsWith("text/")) {
            response.setHeader("Content-Disposition", "inline;filename=" + 
                    encodeFilename(fileInfo.getOriginalName()));
        }
        response.setHeader("Content-Length", String.valueOf(file.length()));
        
        // 写入响应流
        try (FileInputStream inputStream = new FileInputStream(file);
             ServletOutputStream outputStream = response.getOutputStream()) {
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            outputStream.flush();
        } catch (IOException e) {
            log.error("预览文件失败：{}", e.getMessage());
            throw new BusinessException("预览文件失败");
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteFile(Long fileId) {
        FileInfo fileInfo = fileInfoMapper.selectById(fileId);
        if (fileInfo == null) {
            return false;
        }
        
        // 标记文件为无效状态
        fileInfo.setStatus(Constants.FileStatus.INVALID);
        fileInfo.setUpdateTime(LocalDateTime.now());
        fileInfoMapper.updateById(fileInfo);
        
        // 删除物理文件（可选，也可以不删除物理文件，只标记为无效）
        File file = new File(uploadPath + fileInfo.getFilePath());
        if (file.exists()) {
            file.delete();
        }
        
        return true;
    }

    @Override
    public List<FileInfoVO> getFileList() {
        LambdaQueryWrapper<FileInfo> queryWrapper = Wrappers.<FileInfo>lambdaQuery()
                .eq(FileInfo::getStatus, Constants.FileStatus.VALID)
                .orderByDesc(FileInfo::getCreateTime);
        List<FileInfo> fileInfoList = fileInfoMapper.selectList(queryWrapper);
        
        return fileInfoList.stream()
                .map(this::convertToFileInfoVO)
                .collect(Collectors.toList());
    }

    @Override
    public FileInfoVO getFileInfo(Long fileId) {
        FileInfo fileInfo = fileInfoMapper.selectById(fileId);
        if (fileInfo == null || fileInfo.getStatus() == Constants.FileStatus.INVALID) {
            throw new BusinessException(404, "文件不存在");
        }
        
        return convertToFileInfoVO(fileInfo);
    }

    @Transactional(rollbackFor = Exception.class)
    public FileInfoVO compressFiles(List<Long> fileIds) {
        if (fileIds == null || fileIds.isEmpty()) {
            throw new BusinessException(400, "请选择要压缩的文件");
        }
        
        // 获取文件信息
        List<FileInfo> fileInfoList = new ArrayList<>();
        for (Long fileId : fileIds) {
            FileInfo fileInfo = fileInfoMapper.selectById(fileId);
            if (fileInfo == null || fileInfo.getStatus() == Constants.FileStatus.INVALID) {
                throw new BusinessException(404, "文件不存在：" + fileId);
            }
            fileInfoList.add(fileInfo);
        }
        
        // 根据年月创建目录
        String relativePath = createYearMonthDir();
        File targetDir = new File(uploadPath + relativePath);
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        
        // 生成压缩文件名
        String zipFilename = "archive_" + System.currentTimeMillis() + ".zip";
        File zipFile = new File(targetDir, zipFilename);
        
        // 创建ZIP文件
        try (ZipArchiveOutputStream zipOutputStream = new ZipArchiveOutputStream(zipFile)) {
            for (FileInfo fileInfo : fileInfoList) {
                File file = new File(uploadPath + fileInfo.getFilePath());
                if (!file.exists()) {
                    continue;
                }
                
                ZipArchiveEntry entry = new ZipArchiveEntry(fileInfo.getOriginalName());
                zipOutputStream.putArchiveEntry(entry);
                
                try (FileInputStream inputStream = new FileInputStream(file)) {
                    IOUtils.copy(inputStream, zipOutputStream);
                }
                
                zipOutputStream.closeArchiveEntry();
            }
        } catch (IOException e) {
            log.error("压缩文件失败：{}", e.getMessage());
            throw new BusinessException("压缩文件失败");
        }
        
        // 计算MD5
        String md5;
        try (FileInputStream inputStream = new FileInputStream(zipFile)) {
            md5 = DigestUtils.md5DigestAsHex(inputStream);
        } catch (IOException e) {
            log.error("计算压缩文件MD5失败：{}", e.getMessage());
            throw new BusinessException("计算压缩文件MD5失败");
        }
        
        // 保存文件信息到数据库
        FileInfo fileInfo = new FileInfo();
        fileInfo.setOriginalName(zipFilename);
        fileInfo.setNewName(zipFilename);
        fileInfo.setFilePath(relativePath + zipFilename);
        fileInfo.setFileSize(zipFile.length());
        fileInfo.setFileType(Constants.FileType.ARCHIVE);
        fileInfo.setFileExt("zip");
        fileInfo.setMd5(md5);
        fileInfo.setStatus(Constants.FileStatus.VALID);
        fileInfo.setDownloadCount(0);
        fileInfo.setCreateTime(LocalDateTime.now());
        fileInfo.setUpdateTime(LocalDateTime.now());
        fileInfoMapper.insert(fileInfo);
        
        return convertToFileInfoVO(fileInfo);
    }

    @Transactional(rollbackFor = Exception.class)
    public List<FileInfoVO> decompressFile(Long fileId) {
        // 获取文件信息
        FileInfo fileInfo = fileInfoMapper.selectById(fileId);
        if (fileInfo == null || fileInfo.getStatus() == Constants.FileStatus.INVALID) {
            throw new BusinessException(404, "文件不存在");
        }
        
        // 检查文件是否为ZIP格式
        if (!"zip".equalsIgnoreCase(fileInfo.getFileExt())) {
            throw new BusinessException(400, "不支持的文件格式，仅支持ZIP文件");
        }
        
        // 获取源文件
        File sourceFile = new File(uploadPath + fileInfo.getFilePath());
        if (!sourceFile.exists()) {
            throw new BusinessException(404, "文件不存在");
        }
        
        // 创建解压目标目录
        String relativePath = createYearMonthDir();
        File targetDir = new File(uploadPath + relativePath);
        if (!targetDir.exists()) {
            targetDir.mkdirs();
        }
        
        List<FileInfo> decompressedFiles = new ArrayList<>();
        
        // 解压文件
        try (FileInputStream fis = new FileInputStream(sourceFile);
             ZipArchiveInputStream zis = new ZipArchiveInputStream(fis)) {
            
            ZipArchiveEntry entry;
            while ((entry = zis.getNextZipEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                
                // 获取文件名和扩展名
                String originalFilename = entry.getName();
                String extension = FilenameUtils.getExtension(originalFilename).toLowerCase();
                
                // 检查文件类型
                checkFileType(extension);
                
                // 生成新的文件名
                String newFilename = generateNewFilename(extension);
                File targetFile = new File(targetDir, newFilename);
                
                // 保存解压后的文件
                try (FileOutputStream fos = new FileOutputStream(targetFile)) {
                    IOUtils.copy(zis, fos);
                }
                
                // 计算文件MD5
                String md5;
                try (FileInputStream inputStream = new FileInputStream(targetFile)) {
                    md5 = DigestUtils.md5DigestAsHex(inputStream);
                }
                
                // 保存文件信息到数据库
                FileInfo newFileInfo = new FileInfo();
                newFileInfo.setOriginalName(originalFilename);
                newFileInfo.setNewName(newFilename);
                newFileInfo.setFilePath(relativePath + newFilename);
                newFileInfo.setFileSize(targetFile.length());
                newFileInfo.setFileType(FileTypeUtil.getFileType(extension));
                newFileInfo.setFileExt(extension);
                newFileInfo.setMd5(md5);
                newFileInfo.setStatus(Constants.FileStatus.VALID);
                newFileInfo.setDownloadCount(0);
                newFileInfo.setCreateTime(LocalDateTime.now());
                newFileInfo.setUpdateTime(LocalDateTime.now());
                fileInfoMapper.insert(newFileInfo);
                
                decompressedFiles.add(newFileInfo);
            }
        } catch (IOException e) {
            log.error("解压文件失败：{}", e.getMessage());
            throw new BusinessException("解压文件失败");
        }
        
        // 返回解压后的文件信息列表
        return decompressedFiles.stream()
                .map(this::convertToFileInfoVO)
                .collect(Collectors.toList());
    }

    private void checkFileType(String extension) {
        List<String> allowedTypeList = Arrays.asList(allowedTypes.split(","));
        if (!allowedTypeList.contains(extension)) {
            throw new BusinessException(400, "文件类型不被支持");
        }
    }

    /**
     * 检查文件大小是否超过限制
     * @param fileSize 文件大小(字节)
     * @throws BusinessException 如果文件大小超过限制
     */
    private void checkFileSize(long fileSize) {
        if (fileSize <= 0) {
            throw new BusinessException(400, "文件大小不能为0");
        }

        if (maxFileSize != null && fileSize > maxFileSize) {
            throw new BusinessException(400, "文件大小超出限制，最大允许" + (maxFileSize / 1024 / 1024) + "MB");
        }
    }

    /**
     * 生成新的随机文件名
     * @param extension 文件扩展名
     * @return 新的文件名 (UUID + 扩展名)
     */
    private String generateNewFilename(String extension) {
        // 生成UUID作为文件名前缀
        String uuid = UUID.randomUUID().toString().replace("-", "");

        // 如果有扩展名则添加扩展名
        if (StringUtils.hasText(extension)) {
            return uuid + "." + extension;
        }

        return uuid;
    }

    /**
     * 创建按年月组织的目录路径（兼容Windows和Linux）
     * @return 相对路径格式如：yyyy/MM/
     */
    private String createYearMonthDir() {
        LocalDate now = LocalDate.now();
        String year = String.valueOf(now.getYear());
        String month = String.format("%02d", now.getMonthValue());

        // 使用File.separator保证跨平台兼容性
        return year + File.separator + month + File.separator;
    }

    /**
     * 记录上传日志
     * @param fileId 文件ID
     * @param fileName 文件名
     * @param fileSize 文件大小
     * @param fileType 文件类型
     * @param uploadIp 上传IP
     * @param uploadStatus 上传状态
     * @param errorMessage 错误信息
     */
    private void saveUploadLog(Long fileId, String fileName, Long fileSize, String fileType,
                              String uploadIp, Integer uploadStatus, String errorMessage) {
        FileUploadLog uploadLog = new FileUploadLog();
        uploadLog.setFileId(fileId);
        uploadLog.setFileName(fileName);
        uploadLog.setFileSize(fileSize);
        uploadLog.setFileType(fileType);
        uploadLog.setUploadIp(uploadIp);
        uploadLog.setUploadStatus(uploadStatus);
        uploadLog.setErrorMessage(errorMessage);
        uploadLog.setUploadTime(LocalDateTime.now());
        
        fileUploadLogMapper.insert(uploadLog);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    private FileInfoVO convertToFileInfoVO(FileInfo fileInfo) {
        FileInfoVO vo = new FileInfoVO();
        BeanUtils.copyProperties(fileInfo, vo);
        vo.setCreateTime(LocalDateTime.now());
        vo.setDownloadUrl(contextPath + "/api/file/download/" + fileInfo.getId());
        return vo;
    }

    private String getContentType(String ext) {
        if ("png".equals(ext)) return "image/png";
        if ("jpg".equals(ext) || "jpeg".equals(ext)) return "image/jpeg";
        if ("pdf".equals(ext)) return "application/pdf";
        if ("txt".equals(ext)) return "text/plain;charset=UTF-8";
        if ("zip".equals(ext)) return "application/zip";
        return "application/octet-stream";
    }

    private String encodeFilename(String filename) {
        try {
            // 使用URL编码处理文件名并包裹双引号
            return URLEncoder.encode(filename, "UTF-8").replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            return filename;
        }
    }

    private void cleanChunks(String fileMd5) {
        // 删除数据库记录
        LambdaQueryWrapper<FileChunk> query = Wrappers.<FileChunk>lambdaQuery()
                .eq(FileChunk::getFileMd5, fileMd5);
        fileChunkMapper.delete(query);

        // 删除物理分片文件
        File chunkDir = new File(uploadPath + "/chunks/" + fileMd5);
        if (chunkDir.exists()) {
            File[] chunkFiles = chunkDir.listFiles();
            if (chunkFiles != null) {
                for (File f : chunkFiles) {
                    f.delete();
                }
            }
            chunkDir.delete();
        }
    }
}